/**
* Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
* Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
* Oracle and Java are trademarks or registered trademarks of Oracle and/or its
* affiliates. Other product and company names mentioned herein may be trademarks
* or trade names of their respective owners. 
* See LICENSE.TXT for license information.
*/
package com.nokia.example.racer.views;

import com.nokia.example.racer.Main;
import com.nokia.example.racer.helpers.ImageLoader;
import com.nokia.example.racer.helpers.Engine;
import com.nokia.example.racer.helpers.KeyCodes;
import com.nokia.example.racer.sensor.AccelerationProvider;
import com.nokia.example.racer.views.components.BrakeButton;
import com.nokia.example.racer.views.components.Item;
import com.nokia.example.racer.views.components.Track;
import com.nokia.example.racer.views.tracks.EightLoop;
import com.nokia.example.racer.views.tracks.LapListener;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.Sprite;

public class GameView
    extends View
    implements LapListener {

    private static final int MAX_SPEED = 8; // car max speed
    private Timer gameTimer;
    private Timer raceTimer;
    private TimerTask clock;
    private long currentLapTime = 0;
    private long bestLapTime = 0;
    private long latestLapTime = 0;
    /*
     * camera coordinates
     */
    private int camX = 0;
    private int camY = 0;
    /*
     * car coordinates
     */
    private int carX = 0;
    private int carY = 0;
    private int speed = 2;
    private int surfaceMultiplier = 0;
    private int friction = 2;
    private int xIncrement = 0;
    private int yIncrement = 0;
    private int currentFrame = 0;
    private int lastFrame = 0;
    /*
     * precalculated angles
     */
    private double[] sins;
    private double[] coss;
    /*
     * image items
     */
    private Item topBar;
    private Item speedMeter;
    private Item startInfo;
    /*
     * brake button
     */
    private BrakeButton brakeButton;
    /*
     * current track
     */
    private Track track;
    /*
     * sprites
     */
    private Sprite carSprite;
    private Sprite countDown;
    private Sprite speedNeedle;
    /*
     * when stopped, car does not move
     */
    private boolean stopped = true;
    protected boolean applyGas = false;
    /*
     * true when turn button clicked
     */
    private boolean turnLeft = false;
    private boolean turnRight = false;
    protected Engine engine;
    public static boolean rateControlSupported; // indicates if the device supports rate control
    /*
     * thread that shows 3 - 2 - 1 - GO!
     */
    private Thread countdownThread;
    private Font smallFont = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_ITALIC,
        Font.SIZE_SMALL);
    private Font bigFont = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_ITALIC,
        Font.SIZE_LARGE);
    private int tilt = 0;
    private int cumulativeTilt;
    private boolean countdownDone = false;
    /*
     * Object providing sensor data
     */
    private AccelerationProvider accelerationProvider;
    
    private boolean sensorsPromptDeclined;  // indicates if the user has declined the sensor security prompt
    private boolean sensorsEnabled;  // indicates if the device supports sensors

    public GameView() {
        super();
        this.setFullScreenMode(true);
        enableSensors();
        loadGraphics();
        preCalculate();
        try {
            /* 
             * Engine includes RateControl. If creating the
             * RateControl inside Engine -class fails, there is no sound at all,
             * because otherwise it would just be monotone sound.
            */
            engine = new Engine();
            rateControlSupported = true;
        } catch (Exception e) {
            rateControlSupported = false;
        }
    }

    /**
     * Called when showing racing view
     */
    public void showNotify() {
        if (!sensorsEnabled) {
            enableSensors();
        }
        /*
         * EightLoop as the default track
         */
        if (track == null) {
            setTrack(new EightLoop(this));
        }

        gameTimer = new Timer();
        g = getGraphics();
        TimerTask coordAndUi = new TimerTask() {

            public void run() {
                updateCoordinates();
                track.checkGatePass(carX, carY);

                render(g);
                flushGraphics();
            }
        };

        TimerTask turning = new TimerTask() {

            public void run() {
                if (!stopped) {
                    turnByRotate();
                    turnByKeyPress();
                }
            }
        };

        TimerTask physics = new TimerTask() {

            public void run() {
                int frame = lastFrame;
                updateForce(frame);
                lastFrame = currentFrame;

                updateFriction();
            }
        };

        gameTimer.schedule(coordAndUi, 0, 40);
        gameTimer.schedule(turning, 0, 75);
        gameTimer.schedule(physics, 0, 200);

        raceTimer = new Timer();
        clock = new TimerTask() {

            public void run() {
                currentLapTime += 10;
            }
        };

        countdownThread = new Thread() {

            public void run() {
                try {
                    if (!countdownDone) {
                        countDown.setFrame(0);
                        Thread.sleep(1000);
                        countDown.nextFrame();
                        Thread.sleep(1000);
                        countDown.nextFrame();
                        Thread.sleep(1000);
                        countDown.nextFrame();
                        Thread.sleep(700);
                        stopped = false;

                        if (!brakeButton.isPressed()) {
                            accelerate();
                        }
                        countdownDone = true;
                    } else {
                        stopped = false;
                    }
                    raceTimer.scheduleAtFixedRate(clock, 10, 10);

                }
                catch (InterruptedException ie) {
                }
            }
        };
        countdownThread.start();
        if (rateControlSupported) {
            engine.on();
        }
    }

    /**
     * Resets all current driving info for new race to be started.
     */
    private void reset() {
        this.carX = track.getCarStartX();
        this.carY = track.getCarStartY();
        this.camX = carX;
        this.camY = carY;
        currentFrame = track.getCarStartFrame();
        carSprite.setFrame(currentFrame);
        speed = 2;
        xIncrement = 0;
        yIncrement = 0;
        currentLapTime = 0;
        bestLapTime = 0;
        latestLapTime = 0;
        stopped = true;
        decelerate();
        track.resetLaps();
        track.resetGates();
        countDown.setFrame(0);
        countdownDone = false;
    }

    /**
     * Accelerates the car
     */
    public void accelerate() {
        applyGas = true;
        if (rateControlSupported) {
            engine.accelerate();
        }
    }

    /**
     * Decelerates the car
     */
    public void decelerate() {
        applyGas = false;
        if (rateControlSupported) {
            engine.decelerate();
        }
    }

    /**
     * Called when a lap is driven. Resets lap timer.
     *
     * @param laps Total laps driven.
     */
    public void lapDriven(int laps) {
        latestLapTime = currentLapTime;
        currentLapTime = 0;
        synchronized (this) {
            raceTimer.cancel();
            raceTimer = new Timer();
            clock = new TimerTask() {

                public void run() {
                    currentLapTime += 10;
                }
            };
            raceTimer.scheduleAtFixedRate(clock, 10, 10);
        }
        if (latestLapTime < bestLapTime || bestLapTime == 0) {
            bestLapTime = latestLapTime;
        }
    }

    /**
     * Changes the current track
     *
     * @param track The new track
     */
    public void setTrack(Track track) {
        this.track = null;
        this.track = track;
        this.carX = track.getCarStartX();
        this.carY = track.getCarStartY();
        currentFrame = track.getCarStartFrame();
        carSprite.setFrame(currentFrame);
    }

    /**
     * Garbage collects track resources.
     */
    public void cleanTrackResources() {
        track = null;
        System.gc();
    }

    /**
     * Handles drags the gesture provider sends.
     *
     * @param startX X coordinate of the start point of the drag
     * @param endX X coordinate of the end point of the drag
     */
    public void handleDrag(int startX, int endX) {
        if (speed > friction) {
            /*
             * check if drag was from left to right
             */
            if (endX > startX) {
                turnRight();
            }
            else {
                turnLeft();
            }
        }
    }

    /**
     * Called when leaving racing view
     */
    protected void hideNotify() {
        countdownThread.interrupt();
        gameTimer.cancel();
        raceTimer.cancel();
        if (rateControlSupported) {
            engine.off();
        }
        stopped = true;
        /*if (accelerationProvider != null) {
            accelerationProvider.close();
        }*/
        reset();
    }

    protected void sizeChanged(int w, int h) {
        super.sizeChanged(w, h);
        setPositions();
    }

    /*
     * Turns the car one frame left
     */
    protected void turnLeft() {
        lastFrame = currentFrame;

        if (currentFrame == 0) {
            currentFrame = carSprite.getFrameSequenceLength() - 1;
        }
        else {
            currentFrame--;
        }
        carSprite.setFrame(currentFrame);
    }

    /*
     * Turns the car one frame right
     */
    protected void turnRight() {
        lastFrame = currentFrame;

        if (currentFrame == carSprite.getFrameSequenceLength() - 1) {
            currentFrame = 0;
        }
        else {
            currentFrame++;
        }
        carSprite.setFrame(currentFrame);
    }

    protected void pointerPressed(int x, int y) {
        /*
         * If sensors are supported by the device, this 
         * is a touch enabled device
         */
        if (isSensorTurning() || sensorsPromptDeclined) {
            brakeButton.pointerPressed(x, y);
        }
    }

    protected void pointerReleased(int x, int y) {
        /*
         * Either sensors are supported and the user has allowed
         * the device to read the sensor data, or the user has
         * declined the security prompt. In any case, this is
         * a touch enabled device.
         */
        if (isSensorTurning() || sensorsPromptDeclined) {
            brakeButton.pointerReleased(x, y);
            accelerate(); // for cases when the release is not happened on the button
        }

        /*
         * menu button
         */
        if ((Main.landscape && x >= 70 && x < 150 && y <= 40) || (!Main.landscape
            && x <= 60 && y <= 40)) {
            showMenu();
        }
    }

    protected void keyPressed(int keyCode) {
        switch (keyCode) {
            case KEY_NUM8: // brake
                Main.getInstance().getGameView().accelerate();
                break;
        }

        int gameAction = getGameAction(keyCode);
        switch (gameAction) {
            case DOWN: // brake
                Main.getInstance().getGameView().decelerate();
                break;
            case KEY_NUM4: // turn left
                turnLeft = true;
                break;
            case KEY_NUM6: // turn right
                turnRight = true;
                break;
            case LEFT: // turn left
                turnLeft = true;
                break;
            case RIGHT: // turn right
                turnRight = true;
                break;
        }
    }

    protected void keyReleased(int keyCode) {
        switch (keyCode) {
            case KeyCodes.RIGHT_SOFTKEY:
                showMenu();
                break;
            case KEY_NUM8:
                Main.getInstance().getGameView().accelerate();
                break;
        }

        int gameAction = getGameAction(keyCode);
        switch (gameAction) {
            case DOWN:
                Main.getInstance().getGameView().accelerate();
                break;
            case KEY_NUM4:
                turnLeft = false;
                break;
            case KEY_NUM6:
                turnRight = false;
                break;
            case LEFT:
                turnLeft = false;
                break;
            case RIGHT:
                turnRight = false;
                break;
            case KEY_NUM0:
                showMenu();
                break;
        }
    }

    /**
     * Tells whether car turning is done by tilting the phone.
     *
     * @return true if car turning is done by tilting the phone
     */
    private boolean isSensorTurning() {
        return Main.sensorsSupported && Main.landscape;
    }

    private void render(Graphics g) {
        int anchor = Graphics.TOP | Graphics.LEFT;

        track.render(g);
        carSprite.paint(g);
        speedMeter.render(g);
        topBar.render(g);
        if (isSensorTurning() || sensorsPromptDeclined) {
            brakeButton.render(g);
        }
        speedNeedle.paint(g);

        if (stopped && !countdownDone) {
            if (Main.gesturesSupported || Main.sensorsSupported) {
                startInfo.render(g);
            }
            countDown.paint(g);
        }

        g.setColor(0xFFFFFF);
        g.setFont(bigFont);
        g.drawString(track.getLaps() + "", View.screenWidth / 2 - 22, 5, anchor);
        g.setFont(smallFont);
        g.drawString((currentLapTime / 1000) + ":"
            + (currentLapTime % 1000 / 10), View.screenWidth
            / 2 + 57, 0, anchor);
        g.drawString((bestLapTime / 1000) + ":" + (bestLapTime % 1000 / 10),
            View.screenWidth
            / 2 + 57, 12, anchor);

    }

    private void loadGraphics() {
        try {
            brakeButton = new BrakeButton();
            ImageLoader loader = ImageLoader.getInstance();
            speedMeter = new Item("/speedo.png");
            topBar = new Item("/top_bar.png");
            /*
             * The logo at the bottom of the screen that appears
             * shortly during the count down is different, depending
             * on whether this is a touch enabled device and whether
             * the user has allowed the device to read sensor data
             * 
             */
            if(isSensorTurning() && !sensorsPromptDeclined) {
                startInfo = new Item("/help_sensors.png");
            }
            else if(!isSensorTurning() && sensorsPromptDeclined) {
                startInfo = new Item("/help_sensorsPromptDeclined.png");
            }
            else if (!isSensorTurning() && !sensorsPromptDeclined) {
                startInfo = new Item("/help.png");
            }
            carSprite = new Sprite(loader.loadImage("/car_sprite.png"), 64, 64);
            carSprite.setRefPixelPosition(32, 32);
            carSprite.setFrame(currentFrame);
            countDown = new Sprite(loader.loadImage("/countdown.png"), 240, 121);
            countDown.setFrame(0);
            speedNeedle = new Sprite(loader.loadImage("/speed_sprite.png"), 46,
                46);
            speedNeedle.setFrame(0);
            setPositions();
        }
        catch (IOException e) {
        }
    }

    private void setPositions() {
        int w = View.screenWidth;
        int h = View.screenHeight;
        int speedMeterWidth = speedMeter.getImage().getWidth();
        int speedMeterHeight = speedMeter.getImage().getHeight();

        brakeButton.setPosition(5, h - brakeButton.getImage().getHeight() - 5);
        topBar.setPosition((w - topBar.getImage().getWidth()) / 2, 0);
        speedMeter.setPosition(w - speedMeterWidth - 5, h - speedMeterHeight - 5);
        countDown.setPosition(w / 2 - countDown.getWidth() / 2, h / 2
            - countDown.getHeight() / 2);
        speedNeedle.setPosition(w - speedMeterWidth / 2 - speedNeedle.getWidth()
            / 2 - 5,
            h - speedMeterHeight / 2 - speedNeedle.getHeight() / 2 - 7);
        if (Main.landscape) {
            startInfo.setPositionRel(0.5f, 0.85f);
        }
        else {
            startInfo.setPositionRel(0.5f, 0.75f);
        }
    }

    private void preCalculate() {
        int frames = carSprite.getFrameSequenceLength();

        /*
         * precalculate angles
         */
        sins = new double[frames];
        coss = new double[frames];
        for (int i = 0; i < frames; i++) {
            int angle = 360 / frames * i;
            sins[i] = Math.sin(Math.toRadians(angle));
            coss[i] = Math.cos(Math.toRadians(angle));
        }
    }

    private void updateForce(int frame) {
        if (frame == -1) {
            frame = currentFrame;
        }
        if (applyGas) {
            if (speed < MAX_SPEED) {
                speed++;
            }
        }
        else if (!applyGas && speed > friction) {
            speed -= 2;
        }

        int speedCalc = ((speed + surfaceMultiplier) - friction);
        if (speedCalc < 0) {
            speedCalc = 0;
        }

        /*
         * calculate speed components
         */
        yIncrement = (int) (speedCalc * coss[frame]);
        xIncrement = (int) (speedCalc * sins[frame]);
        if (speed >= 0 && speed <= 12) {
            int s = speed * 11 / MAX_SPEED;
            if (s < 0) {
                s = 0;
            }
            if (!applyGas && speed == friction) {
                s = 0;
            }
            speedNeedle.setFrame(s);
        }
    }

    /*
     * Updates car coordinates and keeps them inside track
     */
    private void updateCoordinates() {
        int carRadius = carSprite.getWidth() / 2;
        int trackWidth = track.getWidth();
        int trackHeight = track.getHeight();
        int halfWidth = View.screenWidth / 2;
        int halfHeight = View.screenHeight / 2;

        /*
         * Car
         */
        /*
         * x component
         */
        carX += xIncrement;
        if (carX < carRadius) {
            carX = carRadius;
        }
        if (carX > trackWidth - carRadius) {
            carX = trackWidth - carRadius;
        }

        /*
         * y component
         */
        carY -= yIncrement;
        if (carY < carRadius) {
            carY = carRadius;
        }
        if (carY > trackHeight - carRadius) {
            carY = trackHeight - carRadius;
        }

        // Camera
        /*
         * x component
         */
        camX = carX;
        if (camX < halfWidth) {
            camX = halfWidth;
        }
        if (camX > trackWidth - halfWidth) {
            camX = trackWidth - halfWidth;
        }

        /*
         * y component
         */
        camY = carY;
        if (camY < halfHeight) {
            camY = halfHeight;
        }
        if (camY > trackHeight - halfHeight) {
            camY = trackHeight - halfHeight;
        }

        carSprite.setPosition(halfWidth + carX - camX - 32, halfHeight + carY
            - camY - 32);
        track.setPosition(halfWidth - camX, halfHeight - camY);
    }

    /*
     * Changes friction according to ground type
     */
    private void updateFriction() {
        /*
         * get the color of the ground under center of the car
         */
        int[] rgb = new int[1];
        track.getTrackImage().getRGB(rgb, 0, 1, carX, carY, 1, 1);
        int currentRGB = rgb[0];

        /*
         * rgb is in format 0xAARRGGBB where AA is alpha channel values
         */
        int red = (currentRGB & 0x00FF0000) >> 16;
        int green = (currentRGB & 0x0000FF00) >> 8;
        int blue = (currentRGB & 0x000000FF);

        /*
         * if the color values are less than 70, there is probably asphalt
         */
        if (red < 70 && green < 70 && blue < 70) {
            surfaceMultiplier = 0;
        }
        else {
            surfaceMultiplier = -2;
        }
    }

    private void enableSensors() {
       
        if(!sensorsPromptDeclined) {
           
            if (accelerationProvider != null) {
                accelerationProvider.close();
            }
            /*
            * add acceleration listener
            * Either a ClassNotFoundException will be thrown for devices that
            * do not support sensors, which won't allow the accelerationProvider
            * to initialize, or an IOException will be thrown if the device 
            * supports sensors but the user has declined the security prompt that
            * asks whether the application is allowed to read the device's sensor data.
            */
            try{
                accelerationProvider = AccelerationProvider.getProvider(
                    new AccelerationProvider.Listener() {
            
                        public void dataReceived(double ax, double ay, double az) {
                            if (isSensorTurning()) {
                                tilt = (int) ay;
                            }
                        }
                    });
            }  
            catch (Exception e) {
                if(e.getMessage().equals("could not open sensor")) {
                    sensorsPromptDeclined = true;
                }
            }
        
            if (accelerationProvider != null) {
                Main.sensorsSupported = true;
                sensorsEnabled = true;
            }
            else {
                Main.sensorsSupported = false;
                sensorsEnabled = false;
            }
        }
    }

    private void turnByRotate() {
        cumulativeTilt += tilt;

        /*
         * When cumulative tilt reaches +-4, rotate happens. The more device is rotated, the faster tilt increases/decreases.
         */
        if (isSensorTurning()) {
            if (cumulativeTilt > 4) {
                turnRight();
                cumulativeTilt = 0;
            }
            if (cumulativeTilt < -4) {
                turnLeft();
                cumulativeTilt = 0;
            }
        }
    }

    private void turnByKeyPress() {
        if (turnRight) {
            turnRight();
        }
        else if (turnLeft) {
            turnLeft();
        }
    }

    private void showMenu() {
        reset();
        Main.getInstance().showMenu();
    }
}
